Struct Vs Class When To Use Which · Quick recall Q&A

5 min read
Mid-level7 min read
Rapid overview

Quick recall Q&A

  • No separate heap allocation
  • No object header
  • No pointer indirection
  • If they are fields of a heap object, they live inside that object
  • If they are boxed (cast to object or interface), they go to the heap
  • Large structs copied often can hurt performance
Q: When should you choose a struct over a class?

When the type logically represents a single value, is under ~16 bytes, is immutable, and won't be boxed often (Microsoft's Framework Design Guidelines list all four as requirements). Structs reduce GC pressure because they don't get their own heap allocation: a local lives in the stack frame, a field lives inline inside its containing object. This means

A local struct's memory is reclaimed when the method returns; a struct field goes away with the object that holds it. Neither needs its own GC bookkeeping, which is much cheaper than a heap allocation per instance.

public readonly struct Money
{
    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public decimal Amount { get; }
    public string Currency { get; }
}

void Calculate()
{
    Money price = new Money(100, "USD");
    // price is on the stack
} // stack frame is popped → memory reclaimed immediately

⚠️ Structs are not always stack-allocated:

Q: What pitfalls occur when structs are too large?

Copies become expensive, especially when passing by value. This can negate performance gains and increase stack usage. Use in/ref parameters or switch to classes if the struct grows.

Q: How does boxing affect struct performance?

Boxing copies the struct onto the heap and allocates, defeating the GC benefits. Avoid passing structs to APIs expecting object or non-generic interfaces to prevent boxing.

Q: ** Can structs have parameterless constructors?

Since C# 10 you can declare one, but it must be public**. Every struct always had an implicit public parameterless constructor that zeroes all fields. Watch out: default(T) and new T[n] ignore your constructor and still give the zeroed value; only new T() runs it.

Q: How do you prevent copying when passing structs to methods?

Use in (readonly ref) for read-only access, or ref/ref readonly when you need to mutate or avoid copies. This keeps performance predictable for larger structs.

Q: Can structs inherit from classes?

No. Structs are sealed value types that inherit from ValueType. They can implement interfaces but cannot participate in class inheritance hierarchies.

Q: When do structs hurt cache locality?

Rarely—they often improve locality. However, large structs embedded in arrays can cause cache misses due to size. Evaluate data layout to ensure structs remain lean.

Q: How do you model optional structs?

Use Nullable<T> (Tick?). It wraps the struct with a HasValue flag, allowing null-like semantics without resorting to classes.

Q: What about mutability?

Prefer immutable structs to avoid accidental copies followed by mutation. Mutable structs can lead to confusing bugs when copies diverge silently.

Q: How do structs interact with pattern matching and deconstruction?

They support Deconstruct methods and pattern matching just like classes. This makes them ergonomic for lightweight domain data while still keeping value semantics.

Q: Is it true that structs always live on the stack?

No. A struct lives inline wherever its container lives: a local in the stack frame (or a register), a class field inside that object on the heap, an array element inline in the array's heap block. It ends up on the heap as its own object when boxed, and as a field of a compiler-generated class when captured by a lambda or used as a local in an async/iterator method.

Q: What happens on Point b = a; for a struct versus a class?

For a struct the whole instance is copied, so changing b.X leaves a untouched. For a class only the reference is copied, so a and b point at the same object and a change through one shows through the other.

Q: What are Microsoft's four "avoid a struct unless" criteria?

The type logically represents a single value (like a primitive), its instance size is under 16 bytes, it is immutable, and it won't have to be boxed frequently. If any one fails, the guideline says use a class.

Q: Does default(T) run a struct's parameterless constructor?

No. default(T) and array creation (new T[n]) produce the all-zero value and skip the constructor; only new T() runs it. This is why a struct must always be valid in its zeroed state.

Q: What does readonly struct guarantee, and why does it help performance?

Every field is readonly and every property get-only or init, so no member can mutate this. The compiler can then skip the defensive copy it would otherwise make when you call a member through an in parameter or a readonly field. The guarantee is shallow: a referenced List<T> can still be modified.

Q: What is a defensive copy?

A hidden copy the compiler makes before calling a possibly-mutating member on a struct it must not modify (an in parameter, a readonly field). The call works on the copy, so any mutation is lost, and a large struct pays the copy cost every call. readonly struct or readonly members remove it.

Q: What does record struct give you over a plain struct?

Compiler-generated value equality (Equals, GetHashCode, ==, !=, IEquatable<T>), a readable ToString, and Deconstruct for positional records. Its positional properties are read-write; readonly record struct makes them init-only. Available since C# 10.

Q: Can you compare two plain structs with ==?

Not unless the struct defines operator ==; it's a compile error. Equals works via ValueType.Equals, which compares fields and may fall back to reflection (slow) and boxes the argument. Implement IEquatable<T> or use a record struct.

Q: Why does list[0].X = 5; fail to compile when list is a List<Point> of structs?

The indexer returns a copy of the struct, and assigning to a field of a temporary copy would be silently lost, so the compiler rejects it (CS1612). Replace the element instead: list[0] = list[0] with { X = 5 };, or use an array, whose element access is a variable.

Q: Can a struct inherit, be inherited from, or have a finalizer?

No to all three. A struct implicitly derives from System.ValueType and is implicitly sealed. It can implement interfaces, but calling through the interface type boxes it.


See also